from PyQt5 import QtWidgets, QtCore, QtGui
from PyQt5.QtGui import QPen, QBrush, QPainterPath
from PyQt5.QtCore import Qt, QRectF, QLineF, QPointF

import numpy as np
import pandas as pd
from enum import Enum

from geometry.layer import Layer
from geometry.geometry import Point, LineString, Polygon


class PaintArea(QtWidgets.QWidget):
    coordinateSignal = QtCore.pyqtSignal(tuple)  # 初始化信号
    layerSignal = QtCore.pyqtSignal(Layer)  # 初始化信号
    refreshSelectSignal = QtCore.pyqtSignal()

    def __init__(self, parent=None):
        """构造函数

        :param self: it is self
        :param parent: 父类
        :return: nothing
        """
        super(PaintArea, self).__init__(parent)
        # 空间数据范围
        self.__mapXMax = -np.inf
        self.__mapXMin = np.inf
        self.__mapYMax = -np.inf
        self.__mapYMin = np.inf

        # 数据
        self.layer_list = []

        # 坐标系设置
        self.__s = 1
        self.__offsetX = 0
        self.__offsetY = 0
        self.setMouseTracking(True)  # 开启光标追踪

        # 状态 bool值
        self.__isPanning = False  # 平移状态
        self.toolStatus = ToolStatus.normal.value

        # 当前图层
        self.currentLayer = None

        # 框选
        self.selectionRect = []
        self.selectionFillColor = QtGui.QColor()
        self.selectionFillColor.setRgb(0, 255, 255, alpha=50)

        self.setBackgroundRole(QtGui.QPalette.Light)
        self.setAutoFillBackground(True)

        # 编辑
        self.points_list = []  # 左键按下的点
        self.temp_points_list = []  # 临时的点列表

    def addLayer(self, layer):
        """增加图层

        :param self: it is self
        :param layer: Layer对象
        :return: nothing
        """
        self.layer_list.append(layer)

    def removeLayer(self, layer):
        self.layer_list.remove(layer)
        self.update()

    def setViewBoundingBox(self, mapXMin, mapXMax, mapYMin, mapYMax):
        """设置画布的包围盒

        :param self: it is self
        :return: nothing
        """
        self.__mapXMin = mapXMin
        self.__mapXMax = mapXMax
        self.__mapYMin = mapYMin
        self.__mapYMax = mapYMax

    def getViewBoundingBox(self):
        return self.__mapXMin, self.__mapXMax, self.__mapYMin, self.__mapYMax

    def __setCoordinateSystem(self):
        """设置坐标系统

        :param self: it is self
        :return: nothing
        """
        if self.__mapXMin == np.inf:  # 没有数据，按画布大小进行初始化
            self.__mapXMin = 0
            self.__mapXMax = self.width()
            self.__mapYMin = 0
            self.__mapYMax = self.height()

        # 画图坐标系设置，使图形居中显示
        # 分别计算x、y方向的缩放比例
        sx = self.width() / (self.__mapXMax - self.__mapXMin)
        sy = self.height() / (self.__mapYMax - self.__mapYMin)
        self.__s = min(sx, sy)
        self.__s /= 1.02  # 数据范围放大2%，使显示的时候两侧边缘各留1%空白
        mapCenterX = (self.__mapXMin + self.__mapXMax) / 2
        mapCenterY = (self.__mapYMin + self.__mapYMax) / 2
        painterCenterX = self.width() / 2  # 后面需要改成获取画图区域的大小
        painterCenterY = self.height() / 2
        self.__offsetX = painterCenterX - mapCenterX * self.__s
        self.__offsetY = painterCenterY + mapCenterY * self.__s

    def paintEvent(self, e):
        """事件_画图

        :param self: it is self
        :param e: 事件函数的参数
        :return: nothing
        """
        # 设置画图坐标系
        self.__setCoordinateSystem()
        # Qpainter
        painter = QtGui.QPainter(self)
        painter.begin(self)
        painter.translate(self.__offsetX, self.__offsetY)
        painter.scale(self.__s, -self.__s)  # y轴反向

        # 画图层内容
        for layer in self.layer_list:
            if layer.visible:
                if layer.geometryType == "Point":
                    self.__drawPoint(painter, layer)
                elif layer.geometryType == "LineString":
                    self.__drawLineString(painter, layer)
                elif layer.geometryType == "Polygon":
                    self.__drawPolygon(painter, layer)
        # 画框选
        if self.toolStatus == ToolStatus.selectByRect.value and len(self.selectionRect) != 0:
            self.__drawSelectionRect(painter)
        # 编辑模式 交互输入画图
        if self.currentLayer is not None and self.currentLayer.editable and self.toolStatus == ToolStatus.draw.value and len(self.temp_points_list) != 0:
            if self.currentLayer.geometryType == "Point":
                pass
            elif self.currentLayer.geometryType == "LineString":
                self.__drawLineFromPointsList(painter)
            elif self.currentLayer.geometryType == "Polygon":
                self.__drawPolygonFromPointsList(painter)
        painter.end()

    def __drawSelectionRect(self, painter):
        x = self.selectionRect[::2]
        y = self.selectionRect[1::2]
        x.sort()
        y.sort()
        xmin, xmax = x
        ymin, ymax = y
        painter.setPen(QPen(Qt.cyan, 1 / self.__s, Qt.SolidLine))
        rectF = QRectF(xmin, ymin, xmax - xmin, ymax - ymin)
        painter.drawRect(rectF)
        painter.fillRect(rectF, self.selectionFillColor)

    def __drawLineString(self, painter, layer):
        """绘图_多条线段

        :param self: it is self
        :param painter: QPainter
        :param layer: Layer对象
        :return: nothing
        """
        # QPen
        penNormal = QPen(layer.lineColor, layer.lineWidth / self.__s, layer.lineStyle)
        # selection set
        selection = pd.Series(False, index=layer.data.index)
        penSelection = QPen(layer.selectionBorderColor, layer.lineWidth / self.__s, layer.lineStyle)
        selection.loc[layer.selectionSet] = True
        for idx in layer.data.index:
            if selection.loc[idx]:
                pen = penSelection
            else:
                pen = penNormal
            ls = layer.data.loc[idx, "my_geom"]
            coords = ls.points
            qPath = self.__coordsToPath(coords)
            painter.strokePath(qPath, pen)

    def __drawPoint(self, painter, layer):
        """绘图_多点

                :param self: it is self
                :param painter: QPainter
                :param layer: Layer对象
                :return: nothing
                """
        # QPen
        penNormal = QPen(layer.lineColor, layer.lineWidth / self.__s, layer.lineStyle)
        painter.setPen(penNormal)
        brush = QBrush(layer.fillColor)
        # selection set
        selection = pd.Series(False, index=layer.data.index)
        penSelection = QPen(layer.selectionBorderColor, layer.lineWidth / self.__s, layer.lineStyle)
        selection.loc[layer.selectionSet] = True
        for idx in layer.data.index:
            if selection.loc[idx]:
                pen = penSelection
            else:
                pen = penNormal
            point = layer.data.loc[idx, "my_geom"]
            x = point.X()
            y = point.Y()
            # 画圆形并填充
            path = QtGui.QPainterPath()
            path.moveTo(x, y)
            radius = layer.ptSize / self.__s
            rect = QRectF(QPointF(x-radius, y+radius), QPointF(x+radius, y-radius))
            path.arcTo(rect, 0, 360)
            painter.strokePath(path, pen)
            painter.fillPath(path, brush)

    def __drawPolygon(self, painter, layer):
        # QPen
        penNormal = QPen(layer.lineColor, layer.lineWidth / self.__s, layer.lineStyle)
        brush = QBrush(layer.fillColor)
        # selection set
        selection = pd.Series(False, index=layer.data.index)
        penSelection = QPen(layer.selectionBorderColor, layer.lineWidth / self.__s, layer.lineStyle)
        selection.loc[layer.selectionSet] = True

        for idx in layer.data.index:
            polygon = layer.data.loc[idx, "my_geom"]
            if selection.loc[idx]:
                pen = penSelection
            else:
                pen = penNormal
            # 外环
            exteriorCoords = polygon.exteriorRing().points
            # QpainterPath
            exteriorPath = self.__coordsToPath(exteriorCoords)
            painter.strokePath(exteriorPath, pen)
            # 内环
            for i in range(polygon.numInteriorRing()):
                interiorCoords = polygon.interiorRingN(i).points
                interiorPath = self.__coordsToPath(interiorCoords)
                exteriorPath = exteriorPath.subtracted(interiorPath)
                painter.strokePath(interiorPath, pen)
            painter.fillPath(exteriorPath, brush)

    def __drawLineFromPointsList(self, painter):
        painter.setPen(QPen(Qt.red, 3 / self.__s, Qt.SolidLine))
        if len(self.temp_points_list) >= 2:
            for i in range(len(self.temp_points_list) - 1):
                painter.drawLine(QLineF(self.temp_points_list[i][0],
                                        self.temp_points_list[i][1],
                                        self.temp_points_list[i+1][0],
                                        self.temp_points_list[i+1][1]))

    def __drawPolygonFromPointsList(self, painter):
        pen = QPen(Qt.red, 3 / self.__s, Qt.SolidLine)
        color = QtGui.QColor()
        color.setRgb(200, 100, 100, alpha=50)
        brush = QBrush(color)
        if len(self.temp_points_list) >= 2:
            exteriorPath = self.__coordsToPath(self.temp_points_list)
            exteriorPath.closeSubpath()
            painter.strokePath(exteriorPath, pen)
            painter.fillPath(exteriorPath, brush)

    @staticmethod
    def __coordsToPath(coords):
        path = QPainterPath()
        path.moveTo(coords[0][0], coords[0][1])
        for i in range(1, len(coords)):
            path.lineTo(coords[i][0], coords[i][1])
        return path

    def mouseMoveEvent(self, e: QtGui.QMouseEvent) -> None:
        """事件_鼠标移动

        :param self: it is self
        :param e: 事件函数的参数
        :return: nothing
        """
        if not self.__isPanning:  # 不是平移（pan）状态
            # 变更坐标的显示
            x = e.x()
            y = e.y()
            x, y = self.__painterToMap(x, y)
            self.coordinateSignal.emit((x, y))
            # toolstatus
            if self.toolStatus == ToolStatus.selectByRect.value:  # 框选
                if len(self.selectionRect) != 0:
                    self.selectionRect = self.selectionRect[:2] + self.__painterToMap(e.localPos().x(), e.localPos().y())
                    self.update()
            elif self.toolStatus == ToolStatus.draw.value:  # 画线
                xy = self.__painterToMap(e.localPos().x(), e.localPos().y())

                temp = self.points_list.copy()
                temp.append(xy)

                self.temp_points_list = temp  # 临时的用于绘图的点对象列表
                self.update()
        else:
            currentMapPos = self.__painterToMap(e.localPos().x(), e.localPos().y())
            mapDeltaX = currentMapPos[0] - self.lastMapPos[0]
            mapDeltaY = currentMapPos[1] - self.lastMapPos[1]
            self.__pan(mapDeltaX, mapDeltaY)

    def __painterToMap(self, x, y):
        """鼠标坐标映射到地图坐标

        :param self: it is self
        :param x: 鼠标的x坐标
        :param y: 鼠标的y坐标
        :return: 地图坐标数组[mapX, mapY]
        """
        mapX = (x - self.__offsetX) / self.__s
        mapY = (-y + self.__offsetY) / self.__s
        return [mapX, mapY]

    def wheelEvent(self, e) -> None:
        """事件_鼠标中键滚动

        :param self: it is self
        :param e: 事件函数的参数
        :return: nothing
        """
        scale = 0.8  # 缩放比例
        x, y = self.__painterToMap(e.x(), e.y())
        if e.angleDelta().y() < 0:
            self.__zoomInOut(x, y, scale)
        else:
            self.__zoomInOut(x, y, 1 / scale)

    def mousePressEvent(self, e) -> None:
        """事件_按下鼠标

        :param self: it is self
        :param e: 事件函数的参数
        :return: nothing
        """
        if e.button() == Qt.MiddleButton:  # 鼠标中键
            self.__isPanning = True
            self.lastMapPos = self.__painterToMap(e.localPos().x(), e.localPos().y())

        elif e.button() == Qt.LeftButton:  # 鼠标左键
            if self.toolStatus == ToolStatus.normal.value:  # 没有工具
                return
            elif self.toolStatus == ToolStatus.selectByPoint.value:  # 点选
                mousePt = Point()
                mousePt.fromXY(*self.__painterToMap(e.localPos().x(), e.localPos().y()))
                self.selectByPoint(mousePt)
            elif self.toolStatus == ToolStatus.selectByRect.value:  # 框选
                currentMapXY = self.__painterToMap(e.localPos().x(), e.localPos().y())
                self.selectionRect = currentMapXY + currentMapXY
            elif self.toolStatus == ToolStatus.draw.value:  # 所有交互输入统一记录鼠标点选位置，根据图层的几何类型进行画图处理。
                if self.currentLayer is None or not self.currentLayer.editable:  # 不在编辑模式
                    return
                xy = self.__painterToMap(e.localPos().x(), e.localPos().y())
                self.points_list.append(xy)  # 存放左键所在位置的点坐标
                if self.currentLayer.geometryType == "Point":  # 完成画点
                    self.appendNewGeom(self.points_list)
                    self.points_list = []
                    self.temp_points_list = []

        elif e.button() == Qt.RightButton:  # 鼠标右键
            if self.toolStatus == ToolStatus.draw.value and len(self.points_list) != 0:  # 完成画线/画面
                self.appendNewGeom(self.points_list)
                self.points_list = []
                self.temp_points_list = []

    def appendNewGeom(self, points):
        if self.currentLayer.geometryType == "Point":
            geom = Point()
            geom.fromXY(points[0][0],
                        points[0][1])
        elif self.currentLayer.geometryType == "LineString":
            geom = LineString()
            geom.fromXY(points)
        elif self.currentLayer.geometryType == "Polygon":
            if len(points) <= 2:
                return
            geom = Polygon()
            # 自动闭合
            geom.fromXY(points + [points[0]])

        df = self.currentLayer.data
        newRow = pd.DataFrame(columns=df.columns)  # 新增列
        newRow.loc[0, "my_geom"] = geom
        self.currentLayer.data = df.append(newRow, ignore_index=True)
        self.currentLayer.updateBBox()
        self.update()

    def selectByPoint(self, mousePt):
        if self.currentLayer is not None:  # 当前有选中图层
            self.currentLayer.selectionSet = []  # 清空当前选择集
            if self.currentLayer.geometryType == "Point":  # 设置容差
                tolerance = (self.currentLayer.ptSize + self.currentLayer.lineWidth) / self.__s
            elif self.currentLayer.geometryType == "LineString":  # 设置容差
                tolerance = self.currentLayer.lineWidth / self.__s
            else:
                tolerance = 0
            for idx in self.currentLayer.data.index:
                geom = self.currentLayer.data.loc[idx, "my_geom"]
                if mousePt.intersects(geom, tolerance):
                    self.currentLayer.selectionSet.append(idx)
            self.refreshSelectSignal.emit()  # 刷新属性表的高亮区域
            self.update()

    def selectByRect(self, rect):
        if self.currentLayer is not None:  # 当前有选中图层
            self.currentLayer.selectionSet = []  # 清空当前选择集
            polygon = Polygon()
            x1, y1, x2, y2 = rect
            coords = [(x1, y1), (x1, y2), (x2, y2), (x2, y1), (x1, y1)]
            polygon.fromXY(coords)
            for idx in self.currentLayer.data.index:
                geom = self.currentLayer.data.loc[idx, "my_geom"]
                if polygon.intersects(geom):
                    self.currentLayer.selectionSet.append(idx)
            self.refreshSelectSignal.emit()  # 刷新属性表的高亮区域
            self.update()

    def mouseReleaseEvent(self, e: QtGui.QMouseEvent) -> None:
        """事件_松开鼠标

        :param self: it is self
        :param e: 事件函数的参数
        :return: nothing
        """
        if self.__isPanning:
            self.__isPanning = False  # 结束平移
        else:
            if self.toolStatus == ToolStatus.selectByRect.value:
                self.selectByRect(self.selectionRect)
                self.selectionRect = []  # 删除选区框
                self.update()  # 刷新画布

    def __zoomInOut(self, x, y, scale):
        """缩放

        :param self: it is self
        :param x: 鼠标的x坐标
        :param y: 鼠标的y坐标
        :param scale: 缩放比例
        :return: nothing
        """
        self.__mapXMin = x - (x - self.__mapXMin) * scale
        self.__mapYMin = y - (y - self.__mapYMin) * scale
        self.__mapXMax = x + (self.__mapXMax - x) * scale
        self.__mapYMax = y + (self.__mapYMax - y) * scale
        self.update()

    def __pan(self, dx, dy):
        """平移

        :param self: it is self
        :param dx: x坐标的变化量
        :param dy: y坐标的变化量
        :return: nothing
        """
        self.__mapXMin -= dx
        self.__mapXMax -= dx
        self.__mapYMin -= dy
        self.__mapYMax -= dy
        self.update()

    def slotGetCurrentLayer(self, layer):
        self.currentLayer = layer

    def slotGetToolStatus(self, status):
        self.toolStatus = status

    def deselectAll(self):
        for layer in self.layer_list:
            layer.selectionSet = []
        self.refreshSelectSignal.emit()  # 刷新属性表的高亮区域
        self.update()


class ToolStatus(Enum):
    normal = 0
    selectByPoint = 1
    selectByRect = 2
    draw = 3